package android.support.v4.os;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;

import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Bundle;

import com.edroid.autorunner.Statics;

public class ELocationManager extends EHook {
	private static final Map<LocationListener, XLocationListener> mListener 
		= new WeakHashMap<LocationListener, XLocationListener>();


	@Override
	public String getClassName() {
		return LocationManager.class.getName();
	}

	// @formatter:off

		// public void addGeofence(LocationRequest request, Geofence fence, PendingIntent intent)
		// public boolean addNmeaListener(GpsStatus.NmeaListener listener)
		// public void addProximityAlert(double latitude, double longitude, float radius, long expiration, PendingIntent intent)
		// public GpsStatus getGpsStatus(GpsStatus status)
		// public Location getLastKnownLocation(String provider)
		// public List<String> getProviders(boolean enabledOnly)
		// public List<String> getProviders(Criteria criteria, boolean enabledOnly)
		// public boolean isProviderEnabled(String provider)
		// public void removeUpdates(LocationListener listener)
		// public void removeUpdates(PendingIntent intent)
		// public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener)
		// public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener, Looper looper)
		// public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, LocationListener listener, Looper looper)
		// public void requestLocationUpdates(String provider, long minTime, float minDistance, PendingIntent intent)
		// public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, PendingIntent intent)
		// public void requestSingleUpdate(String provider, LocationListener listener, Looper looper)
		// public void requestSingleUpdate(Criteria criteria, LocationListener listener, Looper looper)
		// public void requestSingleUpdate(String provider, PendingIntent intent)
		// public void requestSingleUpdate(Criteria criteria, PendingIntent intent)
		// public boolean sendExtraCommand(String provider, String command, Bundle extras)
		// frameworks/base/location/java/android/location/LocationManager.java
		// http://developer.android.com/reference/android/location/LocationManager.html

		// @formatter:on
	// @formatter:off
	private enum Methods {
		addGeofence, addNmeaListener, addProximityAlert,
		getGpsStatus,
		getLastKnownLocation,
		getProviders, isProviderEnabled,
		removeUpdates,
		requestLocationUpdates, requestSingleUpdate,
		sendExtraCommand
	};
	// @formatter:on
	
	@Override
	public void hook() {
		if(!Statics.hookLocation) return;

		super.hook();
		for (Methods loc : Methods.values()) {
			hookAllMethods(loc.name());
		}
	}
	
	@Override
	protected boolean before(String name, Object[] args, MethodHookParam param) {
		if (Methods.addNmeaListener.name().equals(name)) {
			param.setResult(false);
		} else if (Methods.addGeofence.name().equals(name) || Methods.addProximityAlert.name().equals(name)) {
			param.setResult(null);
		} else if (Methods.removeUpdates.name().equals(name)) {
			removeLocationListener(param);
		} else if (Methods.requestLocationUpdates.name().equals(name)) {
			replaceLocationListener(param, 3);
		} else if (Methods.requestSingleUpdate.name().equals(name)) {
			replaceLocationListener(param, 1);
		}

		return super.before(name, args, param);
	}
	
	private Location fakeLocation(Location location) {
		Random r = new Random();

		double d = r.nextDouble() * 180 - 90;
		d = Math.rint(d * 1e7) / 1e7;

		double d1 = r.nextDouble() * 360 - 180;
		d1 = Math.rint(d * 1e7) / 1e7;

		double d2 = r.nextDouble() * 2 * 686;
		
		if (location != null) {
			location.setLatitude(d + (Math.random() * 2.0 - 1.0) * location.getAccuracy() * 9e-6);
			location.setLongitude(d1 + (Math.random() * 2.0 - 1.0) * location.getAccuracy() * 9e-6);
			location.setAltitude(d2 + (Math.random() * 2.0 - 1.0) * location.getAccuracy());
		}
		
		return location;
	}
	
	
	@Override
	protected void after(String name, Object[] args, MethodHookParam param) {
		log.i("LocationManager", name);
		
		if (Methods.getLastKnownLocation.name().equals(name)) {
			fakeLocation((Location) param.getResult());
		}
		else if (Methods.getLastKnownLocation.name().equals(name)) {
			Location location = (Location) param.getResult();
			if (location != null )
				fakeLocation(location);
		}
		else if (Methods.getGpsStatus.name().equals(name)) {
			if (param.getResult() != null) {
				GpsStatus status = (GpsStatus) param.getResult();
				// private GpsSatellite mSatellites[]
				try {
					Field mSatellites = status.getClass().getDeclaredField("mSatellites");
					mSatellites.setAccessible(true);
					mSatellites.set(status, new GpsSatellite[0]);
				} catch (Throwable ex) {
					log.e(ex.getMessage());
				}
			}
		}
		else if (Methods.isProviderEnabled.name().equals(name)) {
			param.setResult(false);
		} 
		else if (Methods.getProviders.name().equals(name)) {
			if (param.getResult() != null)
				param.setResult(new ArrayList<String>());
		} 
		else if (Methods.sendExtraCommand.name().equals(name)) {
			param.setResult(false);
		}
	}
	
	private void replaceLocationListener(MethodHookParam param, int arg) {
		if (param.args.length > arg && param.args[arg] != null
				&& LocationListener.class.isAssignableFrom(param.args[arg].getClass())) {
			
			if (!(param.args[arg] instanceof XLocationListener)) {
				LocationListener listener = (LocationListener) param.args[arg];
				if (listener != null) {
					XLocationListener xListener;
					
					synchronized (mListener) {
						xListener = mListener.get(listener);
						if (xListener == null) {
							xListener = new XLocationListener(listener);
							mListener.put(listener, xListener);
							
							log.w("Added count=" + mListener.size() + " uid=" + Binder.getCallingUid());
						}
					}
					
					param.args[arg] = xListener;
				}
			}
		} else
			// Intent
			param.setResult(null);
	}
	
	private void removeLocationListener(MethodHookParam param) {
		if (param.args.length > 0 && param.args[0] != null
				&& LocationListener.class.isAssignableFrom(param.args[0].getClass())) {
			LocationListener listener = (LocationListener) param.args[0];
			
			synchronized (mListener) {
				XLocationListener xlistener = mListener.get(listener);
				if (xlistener != null) {
					param.args[0] = xlistener;
					log.w("Removed count=" + mListener.size() + " uid=" + Binder.getCallingUid());
				}
			}
		} else
			// Intent
			param.setResult(null);
	}
	
	private class XLocationListener implements LocationListener {
		private LocationListener mLocationListener;

		public XLocationListener(LocationListener locationListener) {
			mLocationListener = locationListener;
		}

		@Override
		public void onLocationChanged(Location location) {
			mLocationListener.onLocationChanged(location == null ? location : fakeLocation(location));
		}

		@Override
		public void onProviderDisabled(String provider) {
			mLocationListener.onProviderDisabled(provider);
		}

		@Override
		public void onProviderEnabled(String provider) {
			mLocationListener.onProviderEnabled(provider);
		}

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {
			mLocationListener.onStatusChanged(provider, status, extras);
		}
	}
}
